#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <iostream>
#include <CL/cl.h>
#include <CL/opencl.h>
#include <sys/time.h>
#include <unistd.h>
#include <string.h>
#include <sys/stat.h>

using namespace std;

#define errorMessage(X) X

double timeInMs(struct timeval start, struct timeval end) {
  long seconds  = end.tv_sec  - start.tv_sec;
  long useconds = end.tv_usec - start.tv_usec;
  return ((seconds) * 1000 + useconds/1000.0);
}

char *ReadSource(const char *filename)
{
  struct stat statbuf;
  char* contents = NULL;

  if(stat(filename, &statbuf) != 0) {
    printf("ERROR:Failed to stat %s",filename);
    return NULL;
  }

  FILE *fh;
  if((fh = fopen (filename, "r")) == NULL) {
    printf("ERROR:Failed to open %s",filename);
    return NULL;
  }

  contents = (char*)calloc(1,statbuf.st_size);
  if(contents == NULL) {
      return NULL;
  }


  size_t bytes_left = statbuf.st_size;
  size_t size_in;
  char* ptr = contents;

  while((size_in = fread(ptr, 1, bytes_left, fh)) != 0) {
    ptr+= size_in;
    bytes_left -= size_in;
  }

  if(ferror(fh)) {
    printf("ERROR:Error reading %s",filename);
    return NULL;
  }

  fclose(fh);

  return contents;
}

int main(int argc, char** argv) {

  int data_size = 55* 1000;
  if(argc > 1) {
      int input_size = atoi(argv[1]);
      if(input_size <= 0) {
          printf("Invalid input size: %d. Use default %d\n", data_size, data_size);
      }
      else{
          data_size = input_size;
      }
  }
  int chunk_size = data_size/55 + 1;
  printf("Extend data size to: %d \n", chunk_size * 55);


  //first section
  cl_int error = 0;   // Used to handle error codes
  cl_uint numPlatforms;
  cl_platform_id platforms[10];
  cl_platform_id platform;
  cl_context context;
  cl_command_queue queue;
  cl_device_id device;

  // All platform
  error = clGetPlatformIDs(10, platforms, &numPlatforms);


  // Platform
  if (error != CL_SUCCESS) {
     cout << "Error getting platform id: " << errorMessage(error) << endl;
     exit(error);
  }

  if ( numPlatforms < 2 ) {
      cout << "No OpenCL platforms" << endl;
      exit(1);
  }

  //choose the first one
  // Device:modify the 1:CPU
  // 0: GPU
  platform = platforms[0];


  error = clGetDeviceIDs(platform, CL_DEVICE_TYPE_ALL, 1, &device, NULL);
  if (error != CL_SUCCESS) {
     cout << "Error getting device ids: " << errorMessage(error) << endl;
     exit(error);
  }
  // Context
  context = clCreateContext(0, 1, &device, NULL, NULL, &error);
  if (error != CL_SUCCESS) {
     cout << "Error creating context: " << errorMessage(error) << endl;
     exit(error);
  }
  // Command-queue
  queue = clCreateCommandQueue(context, device, 0, &error);
  if (error != CL_SUCCESS) {
     cout << "Error creating command queue: " << errorMessage(error) << endl;
     exit(error);
  }

  // Creates the program
  char *sources = ReadSource("sha1c.cl"); //read program .cl source file
  if (NULL == sources)
  {
      printf("ERROR: Failed to read sources into memory...\n");
      return -1;
  }


  cl_program program = clCreateProgramWithSource(context, 1, (const char**)&sources, NULL, &error);
  assert(error == CL_SUCCESS);

  // Builds the program
  error = clBuildProgram(program, 1, &device, NULL, NULL, NULL);
  assert(error == CL_SUCCESS);

  // Shows the log
  char* build_log;
  size_t log_size;
  // First call to know the proper size
  clGetProgramBuildInfo(program, device, CL_PROGRAM_BUILD_LOG, 0, NULL, &log_size);
  build_log = new char[log_size+1];
  // Second call to get the log
  clGetProgramBuildInfo(program, device, CL_PROGRAM_BUILD_LOG, log_size, build_log, NULL);
  build_log[log_size] = '\0';
  cout << build_log << endl;
  delete[] build_log;

  // Extracting the kernel
  cl_kernel sha1_kernel = clCreateKernel(program, "sha1", &error);
  assert(error == CL_SUCCESS);

  //Now starting preparing the data
  int msg_size = 64 * chunk_size; //the whole input buffer
  char* msg = (char*)malloc(msg_size * sizeof(char));
  for(int i = 0; i < chunk_size; i++) {
      for(int j=0; j< 55; j++) {
          msg[i*64+j] = (char)(j+32); //input some chars
      }
  }
  int digest_size = 41 * chunk_size;
  char* digest = (char*)malloc(digest_size * sizeof(char));


  struct timeval data_start, data_end, kernel_start, kernel_end;
  gettimeofday(&data_start, NULL);
  // Allocates a buffer of size mem_size and copies mem_size bytes from src_a_h
  cl_mem src_msg = clCreateBuffer(context, CL_MEM_READ_WRITE | CL_MEM_COPY_HOST_PTR, msg_size, msg, &error);
  cl_mem res_digest = clCreateBuffer(context, CL_MEM_WRITE_ONLY, digest_size, NULL, &error);



  // Enqueuing parameters
  // Note that we inform the size of the cl_mem object, not the size of the memory pointed by it
  error = clSetKernelArg(sha1_kernel, 0, sizeof(cl_mem), &src_msg);
  error |= clSetKernelArg(sha1_kernel, 1, sizeof(cl_mem), &res_digest);
  assert(error == CL_SUCCESS);

  // Launching kernel
  // shrRoundUp returns the smallest multiple of local_ws bigger than size
  const size_t global_ws = chunk_size; //shrRoundUp(local_ws, size);    // Total number of work-items


  gettimeofday(&kernel_start, NULL);

  error = clEnqueueNDRangeKernel(queue, sha1_kernel, 1, NULL, &global_ws, NULL, 0, NULL, NULL);
  clFlush(queue);
  clFinish(queue);

  gettimeofday(&kernel_end, NULL);

  clEnqueueReadBuffer(queue, res_digest, CL_TRUE, 0, digest_size, digest, 0, NULL, NULL);

  clFlush(queue);
  clFinish(queue);
  gettimeofday(&data_end, NULL);

  assert(error == CL_SUCCESS);

  printf("[Total:Data+Kernel]Time used %f ms\n", timeInMs(data_start, data_end));
  printf("[Kernel]Time used %f ms\n", timeInMs(kernel_start, kernel_end));
  //try to write some results out
  printf("\nData in\n");
  for(int i = 0; i < 55; i++) {
      putchar(msg[i]);
  }
  printf("\nData out\n");
  for(int i = 0; i < 41; i++) {
      putchar(digest[i]);
  }
  printf("\n");


  // Cleaning up
  free(msg);
  free(digest);
  clReleaseKernel(sha1_kernel);
  clReleaseCommandQueue(queue);
  clReleaseContext(context);
  clReleaseMemObject(src_msg);
  clReleaseMemObject(res_digest);

}
